<?php
// +------------------------------------------------------------------
// | tp61
// | Copyright (c) 2022 All rights reserved.
// | Based on ThinkPHP 6
// | Licensed MulanPSL2( http://license.coscl.org.cn/MulanPSL2 )
// | Author: CLS <422064377>
// | CreateDate: 2022/11/2
// +------------------------------------------------------------------

namespace chleniang\filesystem;


use League\Flysystem\Config;
use League\Flysystem\DirectoryAttributes;
use League\Flysystem\FileAttributes;
use League\Flysystem\FilesystemAdapter;
use League\Flysystem\FilesystemException;
use League\Flysystem\InvalidVisibilityProvided;
use League\Flysystem\PathPrefixer;
use League\Flysystem\UnableToCheckExistence;
use League\Flysystem\UnableToCopyFile;
use League\Flysystem\UnableToCreateDirectory;
use League\Flysystem\UnableToDeleteDirectory;
use League\Flysystem\UnableToDeleteFile;
use League\Flysystem\UnableToMoveFile;
use League\Flysystem\UnableToReadFile;
use League\Flysystem\UnableToRetrieveMetadata;
use League\Flysystem\UnableToWriteFile;
use League\Flysystem\Visibility;
use OSS\Core\OssException;
use OSS\OssClient;

class AliyunOssAdapter implements FilesystemAdapter
{

    protected ?OssClient $ossClient;

    protected string          $accessKeyId;
    protected string          $accessKeySecret;
    protected string          $endpoint;  // 带协议前缀(http:// 或 https://)的endpoint
    protected string          $bucket;
    protected bool            $isCName;
    protected ?string         $securityToken;
    protected ?string         $requestProxy;
    protected null|int|string $timeout;
    protected null|int|string $connectTimeout;

    protected PathPrefixer    $pathPrefixer;
    protected ?FileAttributes $fileAttrs;

    /**
     * aliyun OSS 使用的协议(由endpoint配置参数解析出)
     *      http://  或者  https://
     * @var string
     */
    protected string $schema = 'http://';

    /**
     * 不带协议部分(http:// 或 https://)的endpoint
     * @var string
     */
    protected string $endpointWithoutSchema;

    /**
     * @var array
     */
    protected $aliyunOssDefaultOption = [
        OssClient::OSS_HEADERS => [
            'x-oss-object-acl' => 'default',
        ],
    ];

    /**
     * @param string          $accessKeyId
     * @param string          $accessKeySecret
     * @param string          $endpoint         // 区域
     * @param string          $bucket           //
     * @param boolean         $isCName          // 默认false true为开启CNAME(无法使用listBuckets方法)
     * @param string          $prefix           // 路径前缀(tp配置项为 root)
     * @param string|null     $securityToken    // 默认null from STS
     * @param string|null     $requestProxy     // 默认null 代理服务器地址，
     *                                          例如 "http://<用户名>:<密码>@<代理ip>:<代理端口>"
     * @param int|string|null $timeout
     * @param int|string|null $connectTimeout
     *
     * @throws OssException
     */


    public function __construct(
        string          $accessKeyId,
        string          $accessKeySecret,
        string          $endpoint,
        string          $bucket,
        bool            $isCName = false,
        string          $prefix = '',
        ?string         $securityToken = null,
        ?string         $requestProxy = null,
        null|int|string $timeout = null,
        null|int|string $connectTimeout = null
    )
    {
        $this->accessKeyId     = $accessKeyId;
        $this->accessKeySecret = $accessKeySecret;
        $this->endpoint        = $endpoint;
        $this->bucket          = $bucket;
        $this->isCName         = $isCName;
        $this->securityToken   = $securityToken;
        $this->requestProxy    = $requestProxy;
        $this->timeout         = is_numeric($timeout) ? intval($timeout) : null;
        $this->connectTimeout  = is_numeric($connectTimeout) ? intval($connectTimeout) : null;

        $this->pathPrefixer = new PathPrefixer($prefix, '/'); // DIRECTORY_SEPARATOR

        $this->initOssClient();

        $this->endpointResolve();

    }

    protected function initOssClient()
    {
        $this->ossClient = new OssClient(
            $this->accessKeyId,
            $this->accessKeySecret,
            $this->endpoint,
            $this->isCName,
            $this->securityToken,
            $this->requestProxy
        );
        if (!is_null($this->timeout)) {
            $this->ossClient->setTimeout($this->timeout);
        }

        if (!is_null($this->connectTimeout)) {
            $this->ossClient->setConnectTimeout($this->connectTimeout);
        }
    }

    /**
     * 检查文件是否存在
     *      (flysystem API)
     *
     * @param string $path 文件路径
     *
     * @return bool
     * @throws FilesystemException
     */
    public function fileExists(string $path): bool
    {
        return $this->ossClient->doesObjectExist(
            $this->bucket,
            $this->pathPrefixer->prefixPath($path)
        );
    }

    /**
     * 检查文件目录是否存在
     *      (flysystem API ^3.0 )
     *
     * @param string $path 文件目录路径  目录需以正斜线(/)结尾
     *
     * @return bool
     * @throws FilesystemException
     */
    public function directoryExists(string $path): bool
    {
        return $this->ossClient->doesObjectExist(
            $this->bucket,
            $this->pathPrefixer->prefixDirectoryPath($path)
        );
        // if (!str_ends_with($path, '/')) {
        //     throw new UnableToCheckExistence('Directory must ends with "/"');
        // }
        // return $this->fileExists($path);
    }

    /**
     * 文件写入内容
     *      (flysystem API)
     *
     * @param string $path     要写入的文件路径
     * @param string $contents 文件内容
     * @param Config $config
     *
     * @throws FilesystemException
     */
    public function write(string $path, string $contents, Config $config): void
    {
        $defaultOption = [
            OssClient::OSS_HEADERS => [
                'x-oss-object-acl' => 'default',
            ],
        ];

        $configArr = $config->get(OssClient::OSS_HEADERS, []);

        $option = array_merge_recursive($defaultOption, $configArr);
        $this->ossClient->putObject(
            $this->bucket,
            $this->pathPrefixer->prefixPath($path),
            $contents,
            $option
        );
    }

    /**
     * 文件写入内容
     *      (flysystem API)
     *
     * @param string   $path     要写入的文件路径
     * @param resource $contents 文件内容
     * @param Config   $config
     *
     * @throws FilesystemException
     */
    public function writeStream(string $path, $contents, Config $config): void
    {
        $_data = '';

        while (!feof($contents)) {
            $_data .= fread($contents, 2048);
        }

        $this->write($path, $_data, $config);
    }

    /**
     * 读取文件内容
     *      (flysystem API)
     *
     * @param string $path 要读取的文件路径
     *
     * @return string
     * @throws FilesystemException
     */
    public function read(string $path): string
    {
        return $this->ossClient->getObject(
            $this->bucket,
            $this->pathPrefixer->prefixPath($path)
        );
    }

    /**
     * 读取文件内容(以资源类型返回)
     *      (flysystem API)
     *
     * @param string $path 要读取的文件路径
     *
     * @return resource
     *
     * @throws FilesystemException
     */
    public function readStream(string $path)
    {
        $resource = fopen("php://temp", "w+b");

        try {
            $option = [OssClient::OSS_FILE_DOWNLOAD => $resource];
            $this->ossClient->getObject(
                $this->bucket,
                $this->pathPrefixer->prefixPath($path),
                $option
            );
        }
        catch (OssException|FilesystemException|\Exception $e) {
            fclose($resource);
            throw $e;
        }

        rewind($resource);
        return $resource;
    }

    /**
     * 删除文件
     *      (flysystem API)
     *
     * @param string $path 要删除的文件路径
     *
     * @throws FilesystemException
     */
    public function delete(string $path): void
    {
        $this->ossClient->deleteObject(
            $this->bucket,
            $this->pathPrefixer->prefixPath($path)
        );
    }

    /**
     * 删除文件目录
     *      (flysystem API)
     *
     * @param string $path 要删除的目录路径 目录需以正斜线(/)结尾
     *
     * @throws FilesystemException
     */
    public function deleteDirectory(string $path): void
    {
        // if (!str_ends_with($path, '/')) {
        //     throw new UnableToDeleteDirectory('Directory must ends with "/"');
        // }

        $option = [
            OssClient::OSS_MARKER => null,
            // 填写待删除目录的完整路径，完整路径中不包含Bucket名称。
            OssClient::OSS_PREFIX => $this->pathPrefixer->prefixDirectoryPath($path),
        ];

        $bool = true;
        while ($bool) {
            $result  = $ossClient->listObjects($this->bucket, $option);
            $objects = [];
            if (count($result->getObjectList()) > 0) {
                foreach ($result->getObjectList() as $key => $info) {
                    $objects[] = $info->getKey();
                }
                // 删除目录及目录下的所有文件。
                $delObjects = $ossClient->deleteObjects($this->bucket, $objects);
            }

            if ($result->getIsTruncated() === 'true') {
                $option[OssClient::OSS_MARKER] = $result->getNextMarker();
            }
            else {
                $bool = false;
            }
        }
    }

    /**
     * 创建目录
     *      (flysystem API )
     *
     * @param string $path   文件目录路径 目录需以正斜线(/)结尾
     * @param Config $config 配置
     *
     * @return void
     * @throws FilesystemException
     */
    public function createDirectory(string $path, Config $config): void
    {
        // if (!str_ends_with($path, '/')) {
        //     throw new UnableToCreateDirectory('Directory must ends with "/"');
        // }

        $option = $config->get(OssClient::OSS_HEADERS, null);

        $this->ossClient->putObject(
            $this->bucket,
            $this->pathPrefixer->prefixDirectoryPath($path),
            '',
            $option
        );
    }

    /**
     * 设置文件/目录可见性
     *      (flysystem API )
     *
     * @param string $path       文件/目录路径 目录需以正斜线(/)结尾
     * @param string $visibility 可见性 Visibility::PRIVATE / Visibility::PUBLIC
     *
     * @return void
     * @throws FilesystemException
     */
    public function setVisibility(string $path, string $visibility): void
    {
        $acl = VisibilityConvertor::visibility2Acl($visibility);
        $this->ossClient->putObjectAcl(
            $this->bucket,
            $this->pathPrefixer->prefixPath($path),
            $acl
        );
    }

    /**
     * 获取文件/目录可见性
     *      (flysystem API )
     *
     * @param string $path 文件/目录路径 目录需以正斜线(/)结尾
     *
     * @return FileAttributes
     * @throws FilesystemException
     */
    public function visibility(string $path): FileAttributes
    {
        if (is_null($this->fileAttrs)) {
            $this->normalizeFileAttrs($path);
        }
        if (is_null($this->fileAttrs->visibility())) {
            $objectAcl       = $this->ossClient->getObjectAcl($this->bucket, $path);
            $visibility      = VisibilityConvertor::acl2Visibility($objectAcl);
            $this->fileAttrs = new FileAttributes(
                $this->fileAttrs->path(),
                $this->fileAttrs->fileSize(),
                $visibility,
                $this->fileAttrs->lastModified(),
                $this->fileAttrs->mimeType(),
                $this->fileAttrs->extraMetadata()
            );
        }
        return $this->fileAttrs;
    }

    /**
     * 获取文件mimeType
     *      (flysystem API )
     *
     * @param string $path 文件路径
     *
     * @return FileAttributes
     * @throws FilesystemException
     */
    public function mimeType(string $path): FileAttributes
    {
        return $this->normalizeFileAttrs($path);
    }

    /**
     * 获取文件最后修改时间(时间戳)
     *      (flysystem API )
     *
     * @param string $path 文件路径
     *
     * @return FileAttributes
     * @throws FilesystemException
     */
    public function lastModified(string $path): FileAttributes
    {
        return $this->normalizeFileAttrs($path);
    }

    /**
     * 获取文件大小
     *      (flysystem API )
     *
     * @param string $path 文件路径
     *
     * @return FileAttributes
     * @throws FilesystemException
     */
    public function fileSize(string $path): FileAttributes
    {
        return $this->normalizeFileAttrs($path);
    }

    /**
     * 获取目录内容列表
     *      默认只获取一级,可指定$recursive=true获取所有层级内容列表
     *      (flysystem API)
     *      返回结果是列表形式,使用示例如下:
     *      foreach ($listing as $item) {
     *           // 路径
     *           $path = $item->path();
     *           if ($item instanceof \League\Flysystem\FileAttributes) {
     *               // 文件类型
     *           } elseif ($item instanceof \League\Flysystem\DirectoryAttributes) {
     *               // 目录类型
     *           }
     *      }
     *
     * @param string $path 目录路径
     * @param bool   $deep 是否递归获取(默认false)
     *
     * @return iterable
     * @throws FilesystemException
     */
    public function listContents(string $path, bool $deep): iterable
    {
        $option = array(
            OssClient::OSS_PREFIX        => $this->pathPrefixer->prefixDirectoryPath($path),
            // 分隔符
            OssClient::OSS_DELIMITER     => $deep ? '' : '/',
            // 最大列举个数
            // OssClient::OSS_MAX_KEYS      => 1000,
            // 指定文件名称编码方式为URL。
            OssClient::OSS_ENCODING_TYPE => 'url',
        );

        try {
            $listObjectInfo = $this->ossClient->listObjectsV2($this->bucket, $option);
        }
        catch (OssException $e) {
            throw $e;
            return;
        }
        // 文件
        $objectList = $listObjectInfo->getObjectList();
        // 子文件夹
        $prefixList = $listObjectInfo->getPrefixList();

        if (!empty($objectList)) {
            // 文件
            foreach ($objectList as $object) {
                $lastModified = $object->getLastModified();
                yield new FileAttributes(
                    $object->getKey(),
                    intval($object->getSize()),
                    null,
                    empty($lastModified) ? null : strtotime($lastModified),
                    null,
                    [
                        'type' => $object->getType(),
                    ]
                );;

            }
        }

        if (!empty($prefixList)) {
            // 子文件夹
            foreach ($prefixList as $prefixInfo) {
                yield new DirectoryAttributes($prefixInfo->getPrefix());
            }
        }
    }

    /**
     * 移动文件
     *      (flysystem API )
     *
     * @param string $source      源文件路径
     * @param string $destination 目标文件路径
     * @param Config $config      配置
     *
     * @return void
     * @throws FilesystemException
     */
    public function move(string $source, string $destination, Config $config): void
    {
        try {
            $this->copy($source, $destination, $config);
            $this->delete($source);
        }
        catch (OssException|\Exception $e) {
            throw $e;
        }
    }

    /**
     * 复制文件
     *      (flysystem API )
     *
     * @param string $source      源文件路径
     * @param string $destination 目标文件路径
     * @param Config $config      配置
     *
     * @return void
     * @throws FilesystemException
     */
    public function copy(string $source, string $destination, Config $config): void
    {
        $defaultOption = [
            OssClient::OSS_HEADERS => [
                // 指定CopyObject操作时是否覆盖同名目标Object。此处设置为true，表示禁止覆盖同名Object。
                'x-oss-forbid-overwrite'   => 'true',
                // 如果源Object的ETag值和您提供的ETag相等，则执行拷贝操作，并返回200 OK。
                // 'x-oss-copy-source-if-match' => '5B3C1A2E053D763E1B002CC****',
                // 如果源Object的ETag值和您提供的ETag不相等，则执行拷贝操作，并返回200 OK。
                // 'x-oss-copy-source-if-none-match' => '5B3C1A2E053D763E1B002CC****',
                // 如果指定的时间等于或者晚于文件实际修改时间，则正常拷贝文件，并返回200 OK。
                // 'x-oss-copy-source-if-unmodified-since' => gmdate('2021-12-09T07:01:56.000Z'),
                // 如果指定的时间早于文件实际修改时间，则正常拷贝文件，并返回200 OK。
                // 'x-oss-copy-source-if-modified-since' => gmdate('2021-12-09T07:01:56.000Z'),
                // 指定设置目标Object元信息的方式。此处设置为COPY，表示复制源Object的元数据到目标Object。
                'x-oss-metadata-directive' => 'COPY',
                // 指定OSS创建目标Object时使用的服务器端加密算法。
                // 'x-oss-server-side-encryption' => 'KMS',
                // 表示KMS托管的用户主密钥，该参数仅在x-oss-server-side-encryption为KMS时有效。
                // 'x-oss-server-side-encryption-key-id' => '9468da86-3509-4f8d-a61e-6eab****',
                // 指定OSS创建目标Object时的访问权限。此处设置为private，表示只有Object的拥有者和授权用户有该Object的读写权限，其他用户没有权限操作该Object。
                // 'x-oss-object-acl' => 'private',
                // 指定Object的存储类型。此处设置为Standard，表示标准存储类型。
                // 'x-oss-storage-class' => 'Standard',
                // 指定Object的对象标签，可同时设置多个标签。
                // 'x-oss-tagging' => 'k1=v1&k2=v2&k3=v3',
                // 指定设置目标Object对象标签的方式。此处设置为COPY，表示复制源Object的对象标签到目标Object。
                // 'x-oss-tagging-directive' => 'COPY',
            ],
        ];
        $configArr     = $config->get(OssClient::OSS_HEADERS, []);
        $option        = array_merge_recursive($defaultOption, $configArr);
        $this->ossClient->copyObject(
            $this->bucket,
            $this->pathPrefixer->prefixPath($source),
            $this->bucket,
            $this->pathPrefixer->prefixPath($destination),
            $option
        );
    }

    protected function normalizeFileAttrs($path)
    {
        if (is_null($this->fileAttrs)) {
            $data = $this->ossClient->getObjectMeta(
                $this->bucket,
                $this->pathPrefixer->prefixPath($path)
            );

            $fileSize     = $data[OssClient::OSS_CONTENT_LENGTH] ?? null;
            $lastModified = empty($data[OssClient::OSS_LAST_MODIFIED]) ? null : strtotime($data[OssClient::OSS_LAST_MODIFIED]);

            $this->fileAttrs = new FileAttributes(
                $this->pathPrefixer->prefixPath($path),
                is_null($fileSize) ? null : intval($fileSize),
                null,
                $lastModified,
                $data[OssClient::OSS_CONTENT_TYPE] ?? null,
                [
                    OssClient::OSS_ETAG              => $data[OssClient::OSS_TAGGING] ?? "",
                    OssClient::OSS_HEADER_VERSION_ID => $data[OssClient::OSS_HEADER_VERSION_ID] ?? "",
                ]
            );
        }
        return $this->fileAttrs;
    }


    # adapter API ###############

    /**
     * 获取Aliyun OSSClient对象
     */
    public function getOssClient()
    {
        return $this->ossClient;
    }

    public function setBucket($bucket)
    {
        $this->bucket = $this->bucket;
    }

    /**
     *  获取外网访问地址
     *      阿里云外网访问OSS URL格式
     *      <Schema>://<Bucket>.<外网Endpoint>/<Object>
     *
     * @param string $path
     *
     * @return string
     */
    public function getUrl(string $path): string
    {
        return $this->schema
            . rtrim($this->bucket, '/')
            . rtrim($this->endpointWithoutSchema, '/')
            . '/'
            . ltrim($this->pathPrefixer->prefixPath($path), '/');
    }

    /**
     *
     * @param string $path
     *
     * @return string
     */
    public function url(string $path): string
    {
        return $this->getUrl($path);
    }

    /**
     * 解析 endpoint 配置参数
     *      将协议赋值给 $this->schema
     *      去掉协议部分赋值给 $this->endpointWithoutSchema,以便后期使用不要协议部分的endpoint
     *
     * @return void
     */
    private function endpointResolve()
    {
        $_endpoint = strtolower($this->endpoint);
        if (strpos($_endpoint, 'http://') === 0) {
            $this->endpointWithoutSchema = substr($this->endpoint, strlen('http://'));
            $this->schema                = 'http://';
        }
        elseif (strpos($_endpoint, 'https://') === 0) {
            $this->endpointWithoutSchema = substr($this->endpoint, strlen('https://'));
            $this->schema                = 'https://';
        }
        else {
            $this->endpointWithoutSchema = $this->endpoint;
        }
    }

}